﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using Renci.iRods.Common;
using Renci.iRods.Exceptions;

namespace Renci.iRods.Packets
{
    /// <summary>
    /// Represents base class for all packets
    /// </summary>
    internal abstract class Packet
    {
        private static object _lock = new object();

        private static IList<PacketTypeMetaData> _packets;

        public static IEnumerable<PacketTypeMetaData> Packets
        {
            get
            {
                if (_packets == null)
                {
                    lock (_lock)
                    {
                        if (_packets == null)
                        {
                            //  Build packets metadata information
                            _packets = (from type in typeof(Packet).Assembly.GetTypes()
                                        from packetAttribute in type.GetCustomAttributes(false).OfType<PacketAttribute>()
                                        select new PacketTypeMetaData
                                        {
                                            Type = type,
                                            XmlName = packetAttribute.Name,
                                            Properties = (from prop in type.GetProperties()
                                                          from attribute in prop.GetCustomAttributes(typeof(PacketFieldAttribute), true).OfType<PacketFieldAttribute>()
                                                          select new PacketTypeMetaDataProperty
                                                          {
                                                              Property = prop,
                                                              Field = attribute,
                                                              EnumValues = (from f in prop.PropertyType.GetFields(BindingFlags.Public | BindingFlags.Static)
                                                                            from attr in f.GetCustomAttributes(typeof(PacketEnumAttribute), true).OfType<PacketEnumAttribute>()
                                                                            select new
                                                                            {
                                                                                Name = f.Name,
                                                                                Value = attr.Value
                                                                            }).ToDictionary((i) => i.Name, (i) => i.Value),
                                                          }).ToList(),
                                        }).ToList();
                        }
                    }
                }

                return _packets;
            }
        }

        private PacketTypeMetaData _metaData;
        /// <summary>
        /// Gets the meta data for this type.
        /// </summary>
        protected PacketTypeMetaData MetaData
        {
            get
            {
                if (this._metaData == null)
                {
                    this._metaData = (from p in Packet.Packets
                                      where p.Type == this.GetType()
                                      select p).Single();
                }

                return this._metaData;
            }
        }

        /// <summary>
        /// Packs packet as XML array.
        /// </summary>
        /// <returns></returns>
        public byte[] PackAsXml()
        {
            return Encoding.ASCII.GetBytes(this.ToXml().ToString());
        }

        public byte[] PackAsBinary()
        {
            return this.ToBinary();
        }

        /// <summary>
        /// Unpacks packet from XML string.
        /// </summary>
        /// <param name="xml">The XML.</param>
        public void Unpack(XElement xml)
        {
            this.FromXml(xml);
        }

        public static Packet Load(XElement xml)
        {
            //  Get packet type
            var packetType = (from p in Packet.Packets
                              where p.XmlName == xml.Name.LocalName
                              select p.Type).SingleOrDefault();

            if (packetType == null)
                throw new IRodsException(string.Format("Type '{0}' is not supported.", xml.Name.LocalName));

            var packet = Activator.CreateInstance(packetType) as Packet;

            //  Load packet
            packet.Unpack(xml);

            return packet;

        }

        internal virtual string ToXml()
        {
            //  Build XML representation
            StringBuilder xml = new StringBuilder();
            xml.AppendFormat("<{0}>", this.MetaData.XmlName);
            foreach (var property in this.MetaData.Properties)
            {
                var xmlField = new StringBuilder();

                if (property.Property.PropertyType == typeof(byte[]))
                {
                    //  Handle byte[] as a special case
                    var value = property.Property.GetValue(this, null);
                    var textValue = property.ToXmlTextValue(value);
                    xml.AppendFormat("<{0}>{1}</{0}>", property.Field.XmlName, textValue);
                }
                else if (property.Property.PropertyType.IsArray)
                {
                    Array array = property.Property.GetValue(this, null) as Array;
                    if (array != null)
                    {
                        foreach (var item in array)
                        {
                            var textValue = property.ToXmlTextValue(item);
                            xml.AppendFormat("<{0}>{1}</{0}>", property.Field.XmlName, textValue);
                        }
                    }
                }
                else
                {
                    var value = property.Property.GetValue(this, null);
                    var textValue = property.ToXmlTextValue(value);

                    if (property.Field.DataType == DataTypes.Packet)
                    {
                        xml.Append(textValue);
                    }
                    else 
                    {
                        xml.AppendFormat("<{0}>{1}</{0}>", property.Field.XmlName, textValue);
                    }
                }
            }

            xml.AppendFormat("</{0}>", this.MetaData.XmlName);

            //  TODO:   Define DEBUG levels, like on the server implementation
            Debug.WriteLine(xml);
            return xml.ToString();
        }

        protected virtual byte[] ToBinary()
        {
            throw new NotImplementedException();
        }

        protected virtual void FromXml(XElement xml)
        {
            //  Ensure node type matches type
            if (xml.Name.LocalName != this.MetaData.XmlName)
                throw new InvalidOperationException(string.Format("{0} is expected, {1} received.", this.MetaData.XmlName, xml.Name.LocalName));

            foreach (var property in this.MetaData.Properties)
            {
                var values = (from x in xml.Elements()
                              where x.Name.LocalName == property.Field.XmlName
                              select x).ToList();

                object propertyValue = null;

                if (property.Property.PropertyType == typeof(byte[]))
                {
                    //  Handle byte[] as a special case
                    propertyValue = property.FromXmlValue(values.Single());
                }
                else if (property.Property.PropertyType.IsArray)
                {
                    var array = Activator.CreateInstance(property.Property.PropertyType, values.Count) as Array;
                    for (int i = 0; i < values.Count; i++)
                    {
                        var itemValue = property.FromXmlValue(values[i]);
                        array.SetValue(itemValue, i);
                    }
                    propertyValue = array;
                }
                else
                {
                    propertyValue = property.FromXmlValue(values.SingleOrDefault());
                }

                if (propertyValue == null)
                    continue;

                property.Property.SetValue(this, propertyValue, null);
            }
        }

        protected virtual void FromBinary(byte[] data)
        {
            throw new NotImplementedException();
        }
    }
}
